// Validate dropUser performed via transaction.
// @tags: [requires_replication,exclude_from_large_txns]

import {ReplSetTest} from "jstests/libs/replsettest.js";

function runTest(conn, testCB) {
    const admin = conn.getDB('admin');
    const test = conn.getDB('test');
    admin.createUser({user: 'admin', pwd: 'pwd', roles: ['__system']});
    admin.auth('admin', 'pwd');

    // user1 -> role2 -> role1
    //      \___________.^
    assert.commandWorked(test.runCommand({createRole: 'role1', roles: [], privileges: []}));
    assert.commandWorked(test.runCommand({createRole: 'role2', roles: ['role1'], privileges: []}));
    assert.commandWorked(
        test.runCommand({createUser: 'user1', roles: ['role1', 'role2'], pwd: 'pwd'}));

    const beforeDrop = assert.commandWorked(test.runCommand({usersInfo: 'user1'})).users[0].roles;
    assert.eq(beforeDrop.length, 2);
    assert.eq(beforeDrop.map((r) => r.role).sort(), ['role1', 'role2']);

    testCB(test);

    // Callback should end up dropping role1
    // And we should have no references left to it.
    const allUsers = assert.commandWorked(test.runCommand({usersInfo: 1})).users;
    assert.eq(allUsers.length, 1);
    assert.eq(allUsers[0]._id, 'test.user1');
    assert.eq(allUsers[0].roles.map((r) => r.role), ['role2']);

    const allRoles = assert.commandWorked(test.runCommand({rolesInfo: 1})).roles;
    assert.eq(allRoles.length, 1);
    assert.eq(allRoles[0]._id, 'test.role2');
    assert.eq(allRoles[0].roles.length, 0);

    admin.logout();
}

//// Standalone
// We don't have transactions in standalone mode.
// Behavior elides transaction machinery, but is still protected by
// local mutex on the UMC commands.
// Expect the second command to block.
{
    const kFailpointDelay = 10 * 1000;
    const mongod = MongoRunner.runMongod({auth: null});
    assert.commandWorked(mongod.getDB('admin').runCommand({
        configureFailPoint: 'umcTransaction',
        mode: 'alwaysOn',
        data: {commitDelayMS: NumberInt(kFailpointDelay)},
    }));

    runTest(mongod, function(test) {
        // Pause and cause next op to block.
        const parallelShell = startParallelShell(
            `
            db.getSiblingDB('admin').auth('admin', 'pwd');
            assert.commandWorked(db.getSiblingDB('test').runCommand({dropRole: 'role1'}));
        `,
            mongod.port);

        // Other UMCs block.
        assert.commandWorked(test.runCommand({updateRole: 'role2', privileges: []}));
        parallelShell();

        jsTest.log("Verify the failpoint is triggered.");
        const kUMCTransactionCommitDelayLogId = 4993100;
        checkLog.containsJson(
            mongod, kUMCTransactionCommitDelayLogId, {durationMillis: kFailpointDelay});
    });

    MongoRunner.stopMongod(mongod);
}

//// ReplicaSet
// Ensure that dropRoles generates a transaction by checking for applyOps.
{
    const rst = new ReplSetTest({nodes: 3, keyFile: 'jstests/libs/key1'});
    rst.startSet();
    rst.initiate();
    rst.awaitSecondaryNodes();

    function relevantOp(op) {
        return ((op.op === 'u') || (op.op === 'd')) &&
            ((op.ns === 'admin.system.users') || (op.ns === 'admin.system.roles'));
    }

    function probableTransaction(op) {
        return (op.op === 'c') && (op.ns === 'admin.$cmd') && (op.o.applyOps !== undefined) &&
            op.o.applyOps.some(relevantOp);
    }

    runTest(rst.getPrimary(), function(test) {
        assert.commandWorked(test.runCommand({dropRole: 'role1'}));
        const oplog = test.getSiblingDB('local').oplog.rs.find({}).toArray();
        jsTest.log('Oplog: ' + tojson(oplog));

        // Events were not executed directly on the collections.
        const updatesAndDrops = oplog.filter(relevantOp);
        assert.eq(updatesAndDrops.length,
                  0,
                  'Found expected actions on priv collections: ' + tojson(updatesAndDrops));

        // They were executed by way of a transaction.
        const txns = oplog.filter(probableTransaction);
        assert.eq(
            txns.length, 1, 'Found unexpected number of probable transactions: ' + tojson(txns));

        const txnOps = txns[0].o.applyOps;
        assert.eq(
            txnOps.length, 3, 'Found unexpected number of ops in transaction: ' + tojson(txnOps));

        // Op1: Remove 'role1' from user1
        const msgUpdateUser = 'First op should be update admin.system.users' + tojson(txnOps);
        assert.eq(txnOps[0].op, 'u', msgUpdateUser);
        assert.eq(txnOps[0].ns, 'admin.system.users', msgUpdateUser);
        assert.eq(txnOps[0].o2._id, 'test.user1', msgUpdateUser);
        assert.eq(txnOps[0].o.diff.u.roles, [{role: 'role2', db: 'test'}], msgUpdateUser);

        // Op2: Remove 'role1' from role2
        const msgUpdateRole = 'Second op should be update admin.system.roles' + tojson(txnOps);
        assert.eq(txnOps[1].op, 'u', msgUpdateRole);
        assert.eq(txnOps[1].ns, 'admin.system.roles', msgUpdateRole);
        assert.eq(txnOps[1].o2._id, 'test.role2', msgUpdateRole);
        assert.eq(txnOps[1].o.diff.u.roles, [], msgUpdateRole);

        // Op3: Remove 'role1' document
        const msgDropRole = 'Third op should be drop from admin.system.roles' + tojson(txnOps);
        assert.eq(txnOps[2].op, 'd', msgDropRole);
        assert.eq(txnOps[2].ns, 'admin.system.roles', msgUpdateRole);
        assert.eq(txnOps[2].o._id, 'test.role1', msgUpdateRole);

        jsTest.log('Oplog applyOps: ' + tojson(txns));
    });

    rst.stopSet();
}
